# Mini Golf recomp — build definition.
#
# Layers (see README.md and PLAN.md "Architecture"):
#   build/gen-pure/      the pure recompilation, generated by tools/emit.py for the exact
#                        oracle only; compiled with relaxed warnings, never edited
#   src/runtime/         guest CPU state, guest memory, ARM C-runtime bindings, frame pump
#   src/framework/       what the game may ask the platform to do: graphics, audio, storage,
#                        controls, device — headers only, one namespace each
#   src/libeapp/         the iPod eApp frameworks implementing them (OpenGLES, Audio, ...)
#   src/game/            the game, hand-decompiled — what the ordinary build compiles
#   src/gamedata/        the manifest of the game's files, verification, first-run install (zlib)
#   src/platform/        paths shared by every platform, then one directory per platform:
#   src/platform/sdl3/   window, audio, input  -> target `minigolf`
#   src/platform/null/   no window, no audio   -> target `minigolf-headless` (tests, CI)
#   src/platform/switch/ libnx framebuffer, pad, audout -> `minigolf-switch.nro` (see README)
#
# Everything hand-written is compiled with warnings as errors; see `minigolf_strict` below.
# Layers whose directory is still empty are skipped with a message rather than failing, so the
# tree configures at every stage of PLAN.md.

cmake_minimum_required(VERSION 3.21)
# The oldest macOS a release runs on, set before `project()` because that is when it is
# read. 11 is the first that ran on Apple silicon, so one number covers both architectures.
if(APPLE AND NOT CMAKE_OSX_DEPLOYMENT_TARGET)
    set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0 CACHE STRING "The oldest macOS this build runs on")
endif()
project(minigolf VERSION 1.0.1 LANGUAGES CXX)
if(APPLE)
    enable_language(OBJCXX)  # the SDL build's menu bar (src/platform/sdl3/macos_menu.mm)
endif()
if(WIN32)
    enable_language(RC)  # the manifest that gives the settings window its controls' modern look
endif()
if(ANDROID)
    # SDL3's CMake package works out the target CPU by compiling a small .c file, which it can
    # only do if C is a language this project has enabled.
    enable_language(C)
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Two configurations, and which one is the default matters more here than it does in most
# projects, because **this program's renderer is a software rasteriser**: there is no GPU to take
# the per-pixel work, so the optimiser's output *is* the frame rate. A default of `-O1` meant
# everyone who followed the README got the slow one.
#
# Release is therefore the default and is what `cmake -B build` gives you.
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
if(NOT MSVC)
    # Release: optimised, and still carrying its symbols and its assertions. `-g` costs nothing at
    # run time and is what makes a profile readable. Dropping CMake's default `-DNDEBUG` keeps
    # `assert` live, which was measured to cost nothing at all, so there is no reason to trade the
    # checks away.
    set(CMAKE_CXX_FLAGS_RELEASE "-O3 -g")
    # RelWithDebInfo is the *debugging* configuration in this project, and -O1 is deliberate: it
    # keeps the generated code steppable — which is how a recompiled function is compared against
    # the decompiled one instruction by instruction — while still folding the obvious redundancy
    # that makes -O0 unusably slow. Ask for it by name when that is what you are doing.
    set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O1 -g")
endif()

# The software rasteriser draws a frame on several threads (src/libeapp/gles.cpp). Every target
# needs the platform's thread library; on a single-core host the pool simply never starts.
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

option(MINIGOLF_SANITIZE "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
# The console memory model (src/runtime/memory.cpp): four allocated regions rather than one big
# mapping. On by default where there is no mmap; this builds it on a desktop as well, so that the
# oracle can be run against the model the console uses.
option(MINIGOLF_REGION_MEMORY "Use the console's guest-memory model on this host too" OFF)
option(MINIGOLF_SDL3 "Build the SDL3 desktop target (needs sdl3 via pkg-config)" ON)
# Where tools/emit.py wrote the recompiled code. Empty (the default) builds the hand-decompiled
# game in src/game/ with no generated code at all, which is the ordinary build. Setting it — as
# tests/check-recomp.sh does, to build/gen-pure — builds the pure recompilation instead, for the
# exact oracle: no src/game/, no unit tests, and the ARM-ABI adapter compiled in.
set(MINIGOLF_GEN_DIR "" CACHE PATH "Directory of generated sources (pure recompilation only)")

# ---------------------------------------------------------------------------------------------
# Compiler settings shared by every target
# ---------------------------------------------------------------------------------------------

# Common: the project never throws and never uses RTTI (the guest code cannot, and neither can
# every target platform cheaply). The three compiler families spell all of this differently, so
# each option set is chosen once here rather than repeated per target.
# The shared core: code that belongs to no single title, compiled once from
# `common/` rather than copied into this tree. See ../common/README.md.
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../common ${CMAKE_CURRENT_BINARY_DIR}/common)
# The shared CMake: the macOS .app, and whatever else every title shapes the same way.
include(${CMAKE_CURRENT_SOURCE_DIR}/../common/cmake/IpodMacBundle.cmake)

add_library(minigolf_common INTERFACE)
if(MSVC)
    target_compile_options(minigolf_common INTERFACE /EHs-c- /GR- /utf-8)
    target_compile_definitions(minigolf_common INTERFACE _CRT_SECURE_NO_WARNINGS)
else()
    target_compile_options(minigolf_common INTERFACE -fno-exceptions -fno-rtti)
endif()
if(MINGW)
    # MinGW's printf is Microsoft's old C runtime's by default, which has no %zu and whose
    # format checks say so under -Werror; this asks for MinGW's own, which is C99.
    target_compile_definitions(minigolf_common INTERFACE __USE_MINGW_ANSI_STDIO=1)
endif()
target_link_libraries(minigolf_common INTERFACE Threads::Threads)
target_link_libraries(minigolf_common INTERFACE ipod_core)
target_include_directories(minigolf_common INTERFACE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    ${CMAKE_CURRENT_SOURCE_DIR}/src/libeapp/include)
if(MINIGOLF_GEN_DIR)
    target_include_directories(minigolf_common INTERFACE ${MINIGOLF_GEN_DIR})
    # The pure recompilation does not link src/game/ at all, so the runtime must not call into
    # it — the port's own start-up (cheats, round history) is compiled out (src/runtime/main.cpp).
    target_compile_definitions(minigolf_common INTERFACE MINIGOLF_PURE_RECOMPILATION)
endif()
if(MINIGOLF_REGION_MEMORY)
    target_compile_definitions(minigolf_common INTERFACE MINIGOLF_REGION_MEMORY)
endif()
if(MINIGOLF_SANITIZE)
    if(MSVC)
        target_compile_options(minigolf_common INTERFACE /fsanitize=address)
    else()
        target_compile_options(minigolf_common INTERFACE -fsanitize=address,undefined
                               -fno-omit-frame-pointer)
        target_link_options(minigolf_common INTERFACE -fsanitize=address,undefined)
    endif()
endif()

# Strict: hand-written code only. Warnings are errors.
add_library(minigolf_strict INTERFACE)
target_link_libraries(minigolf_strict INTERFACE minigolf_common)
if(MSVC)
    target_compile_options(minigolf_strict INTERFACE /W4 /WX)
else()
    target_compile_options(minigolf_strict INTERFACE
        -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Werror)
endif()

# Generated: the recompiled tree is mechanical output. Unused labels and always-true
# conditions are expected
# there; real errors (implicit declarations, bad types) still fail the build.
add_library(minigolf_generated INTERFACE)
target_link_libraries(minigolf_generated INTERFACE minigolf_common)
if(MSVC)
    target_compile_options(minigolf_generated INTERFACE /W3 /WX)
else()
    target_compile_options(minigolf_generated INTERFACE
        -Wall -Werror -Wno-unused-label -Wno-unused-variable -Wno-unused-but-set-variable)
endif()

# ---------------------------------------------------------------------------------------------
# Source discovery. CONFIGURE_DEPENDS re-globs on every build so new files are picked up.
# ---------------------------------------------------------------------------------------------

if(MINIGOLF_GEN_DIR)
    file(GLOB GEN_SOURCES  CONFIGURE_DEPENDS ${MINIGOLF_GEN_DIR}/*.cpp)
    if(NOT GEN_SOURCES)
        # Say it here rather than let the link fail. Without the generated half, everything
        # compiles and the link ends in a page of undefined symbols that names the symptom and
        # not the cause. The generated code is not in the repository on purpose: it is a
        # translation of the game's own binary. See README.md ("Building").
        message(FATAL_ERROR
            "no generated sources in ${MINIGOLF_GEN_DIR}.\n"
            "This title compiles machine-translated code that is not kept in the repository. "
            "Write it first, from your own copy of the game:\n"
            "    python3 tools/funcs.py --image PATH/TO/Executables/<the game>.bin\n"
            "    python3 tools/emit.py\n"
            "See README.md, \"Building\".")
    endif()
endif()
file(GLOB RUNTIME_SOURCES  CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/runtime/*.cpp)
file(GLOB LIBEAPP_SOURCES  CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/libeapp/*.cpp)
if(NOT MINIGOLF_GEN_DIR)
    # The ARM calling convention is only how the pure recompilation reaches the frameworks; the
    # hand-decompiled game calls the typed entry points in src/framework/ directly.
    list(FILTER LIBEAPP_SOURCES EXCLUDE REGEX "/arm_abi\\.cpp$")
endif()
file(GLOB GAME_SOURCES     CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/game/*.cpp)
file(GLOB GAMEDATA_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/gamedata/*.cpp)
file(GLOB PLATFORM_COMMON  CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/*.cpp)
file(GLOB SDL3_SOURCES     CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/sdl3/*.cpp)
# …and the shared core's SDL code, compiled into this title's window build alongside it.
list(APPEND SDL3_SOURCES ${IPOD_CORE_SDL3_SOURCES})
if(APPLE)
    file(GLOB SDL3_OBJC_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/sdl3/*.mm)
    list(APPEND SDL3_SOURCES ${SDL3_OBJC_SOURCES})
endif()
if(WIN32)
    list(APPEND SDL3_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/sdl3/win32_settings.rc)
endif()
file(GLOB NULL_SOURCES     CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/null/*.cpp)
file(GLOB SWITCH_SOURCES   CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/switch/*.cpp)
file(GLOB UNIT_TEST_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/unit/*.cpp)

# main.cpp is the program entry shared by both executables; everything else in src/runtime is
# the runtime library.
list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "/main\\.cpp$")
set(MAIN_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/runtime/main.cpp)

# ---------------------------------------------------------------------------------------------
# Libraries
# ---------------------------------------------------------------------------------------------

set(MINIGOLF_CORE_LIBS "")

if(RUNTIME_SOURCES)
    add_library(minigolf_runtime STATIC ${RUNTIME_SOURCES})
    target_link_libraries(minigolf_runtime PUBLIC minigolf_strict)
    list(APPEND MINIGOLF_CORE_LIBS minigolf_runtime)
else()
    message(STATUS "src/runtime/ has no sources yet — runtime library skipped")
endif()

if(LIBEAPP_SOURCES)
    add_library(minigolf_eapp STATIC ${LIBEAPP_SOURCES})
    target_link_libraries(minigolf_eapp PUBLIC minigolf_strict)
    list(APPEND MINIGOLF_CORE_LIBS minigolf_eapp)
else()
    message(STATUS "src/libeapp/ has no sources yet — eapp library skipped")
endif()

if(GEN_SOURCES)
    add_library(minigolf_gen STATIC ${GEN_SOURCES})
    target_link_libraries(minigolf_gen PUBLIC minigolf_generated)
    list(APPEND MINIGOLF_CORE_LIBS minigolf_gen)
endif()

if(GAMEDATA_SOURCES)
    find_package(ZLIB REQUIRED)  # inflating the game's zip, and the CRC-32 of every file
    add_library(minigolf_gamedata STATIC ${GAMEDATA_SOURCES} ${PLATFORM_COMMON})
    target_link_libraries(minigolf_gamedata PUBLIC minigolf_strict ZLIB::ZLIB)
    list(APPEND MINIGOLF_CORE_LIBS minigolf_gamedata)
endif()

if(GAME_SOURCES AND NOT MINIGOLF_GEN_DIR)
    add_library(minigolf_game STATIC ${GAME_SOURCES})
    target_link_libraries(minigolf_game PUBLIC minigolf_strict)
    list(APPEND MINIGOLF_CORE_LIBS minigolf_game)
endif()

# ---------------------------------------------------------------------------------------------
# Executables
# ---------------------------------------------------------------------------------------------

# Link a program against the hand-written libraries and the shared core. The static libraries
# reference each other in both directions — gen calls libeapp, libeapp calls back into the
# runtime, and the shared core's rasteriser calls this title's call log — which Apple's linker
# and MSVC's resolve in one pass. A GNU-style linker scans each archive once, in order, so those
# go between --start-group and --end-group, which has it rescan them until nothing is left
# unresolved. The core is named by file there rather than by target: as a target it is already
# an interface dependency of every library, and CMake would move it out of the group.
function(minigolf_link_core NAME)
    if(APPLE OR MSVC)
        target_link_libraries(${NAME} PRIVATE minigolf_strict ${MINIGOLF_CORE_LIBS})
    else()
        target_link_libraries(${NAME} PRIVATE minigolf_strict -Wl,--start-group
                              ${MINIGOLF_CORE_LIBS} $<TARGET_FILE:ipod_core> -Wl,--end-group)
    endif()
endfunction()

# Pass SHARED to get a shared library rather than a program. Android is why: there a game is
# loaded from Java by SDL's activity, which calls SDL_main inside a .so, so the windowed build is
# `libminigolf.so` and not an executable at all. The headless build stays a real program
# everywhere, Android included, where it is what runs the tests on the device.
function(minigolf_add_executable NAME PLATFORM_SOURCES)
    if(NOT EXISTS ${MAIN_SOURCE} OR NOT MINIGOLF_CORE_LIBS)
        message(STATUS "${NAME}: runtime not written yet — target skipped")
        return()
    endif()
    if("SHARED" IN_LIST ARGN)
        add_library(${NAME} SHARED ${MAIN_SOURCE} ${PLATFORM_SOURCES})
    else()
        add_executable(${NAME} ${MAIN_SOURCE} ${PLATFORM_SOURCES})
    endif()
    minigolf_link_core(${NAME})
endfunction()

if(NULL_SOURCES)
    minigolf_add_executable(minigolf-headless "${NULL_SOURCES}")
else()
    message(STATUS "src/platform/null/ has no sources yet — minigolf-headless skipped")
endif()

if(MINIGOLF_SDL3 AND SDL3_SOURCES)
    # SDL3 ships a CMake package; fall back to pkg-config for distributions that do not install
    # it (and so that a Windows or console toolchain, which has no pkg-config, still configures).
    #
    # The fallback is for *this* machine only. pkg-config answers about the host, so a
    # cross-build that reached it would quietly link the SDL belonging to the machine doing the
    # building — on a Linux box cross-compiling for Android, an x86_64 library for an arm64
    # program, which fails at the link if you are lucky and at run time if you are not. When
    # cross-compiling, the CMake package is the only answer, and not finding it is an error
    # worth stopping for.
    find_package(SDL3 QUIET CONFIG)
    if(TARGET SDL3::SDL3)
        set(MINIGOLF_SDL3_TARGET SDL3::SDL3)
    elseif(CMAKE_CROSSCOMPILING)
        message(FATAL_ERROR
            "no SDL3 CMake package for this target.\n"
            "A cross-build cannot use the host's pkg-config: it describes the machine doing the "
            "building, not the one being built for. Point SDL3_DIR at an SDL3 built for this "
            "target. SDL's Android release is an .aar, which is also a Python script that "
            "unpacks itself into an ordinary prefix:\n"
            "    python3 SDL3-<version>.aar -o <prefix>\n"
            "then configure with -DSDL3_DIR=<prefix>/lib/cmake/SDL3.")
    else()
        find_package(PkgConfig REQUIRED)
        pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
        set(MINIGOLF_SDL3_TARGET PkgConfig::SDL3)
    endif()
    if(ANDROID)
        minigolf_add_executable(minigolf "${SDL3_SOURCES}" SHARED)
        if(TARGET minigolf)
            # SDL's own activity (org.libsdl.app.SDLActivity, in the .aar) loads "SDL3" and then
            # "main", and calls SDL_main in the second. Being that library is what lets the app
            # ship no Java of its own — no subclass to write, nothing to compile — so the file is
            # named for whatever loads it rather than for the game inside it.
            set_target_properties(minigolf PROPERTIES OUTPUT_NAME main)
            # The NDK's media library, for the AAC decoder the music goes through
            # (common/src/ipod/platform/sdl3/music_decoder.cpp). It is the system's own codec,
            # the same bargain macOS and Windows make, and it ships with every Android since 5.0.
            target_link_libraries(minigolf PRIVATE mediandk)
            # Where the program starts. Android is the one platform where that is SDL_main
            # rather than main: the activity loads this library and calls it. SDL_main.h —
            # included by src/runtime/main.cpp under this name — is what does the renaming.
            #
            # Android alone, deliberately. That header also renames `main` on Windows, and
            # brings in SDL's own WinMain, which builds argv from GetCommandLineW as UTF-8
            # instead of the CRT's ANSI one. Nothing above this reads argv expecting that, so
            # a --gamedir with a non-ASCII character in it would change meaning on Windows for
            # a rename Windows never needed. The headless build compiles the same file without
            # this and keeps a plain main everywhere.
            target_compile_definitions(minigolf PRIVATE MINIGOLF_SDL3_MAIN)
        endif()
    else()
        minigolf_add_executable(minigolf "${SDL3_SOURCES}")
    endif()
    if(TARGET minigolf)
        target_link_libraries(minigolf PRIVATE ${MINIGOLF_SDL3_TARGET})
        if(APPLE)
            # Cocoa for the settings window; AudioToolbox to decode the AAC music, which SDL
            # does not (src/platform/sdl3/music_decoder.cpp). Both are system frameworks.
            target_link_libraries(minigolf PRIVATE "-framework Cocoa" "-framework AudioToolbox")
            # A double-clickable .app carrying its own SDL, rather than a bare executable
            # linked against whatever SDL this machine happens to have.
            # The SDL it linked against is named for the bundle, because a framework's
            # install name says nothing about where the framework came from and there
            # would be nothing for the bundling step to go and find. A pkg-config SDL is
            # a plain dylib and is found the ordinary way, so it is not named here.
            set(MAC_CARRY "")
            if(TARGET SDL3::SDL3)
                set(MAC_CARRY "$<TARGET_FILE:SDL3::SDL3>")
            endif()
            ipod_macos_app_bundle(minigolf "Mini Golf" "org.ipodrecomp.minigolf"
                                  ${PROJECT_VERSION} ${MAC_CARRY})
        elseif(WIN32)
            # The common controls and the visual-styles API, for the settings window
            # (src/platform/sdl3/win32_settings.cpp); Media Foundation and COM to decode the
            # AAC music (music_decoder.cpp). All of them ship with Windows.
            target_link_libraries(minigolf PRIVATE comctl32 uxtheme mfplat mfreadwrite mfuuid
                                  ole32)
            # A game, not a command-line tool: no console window behind the game's own. Its
            # messages still reach a terminal that started it, and anything fatal is a message
            # box (common/src/ipod/platform/windows_console.h). The headless build is
            # deliberately not this: its output is the whole point of it.
            set_target_properties(minigolf PROPERTIES WIN32_EXECUTABLE TRUE)
        endif()
    endif()
elseif(MINIGOLF_SDL3)
    message(STATUS "src/platform/sdl3/ has no sources yet — minigolf skipped")
endif()

# The Nintendo Switch, as homebrew. Built with devkitPro's toolchain file, which sets
# NINTENDO_SWITCH; there is no way to run the result here, so the ordinary build never sees it.
# tools/switch-build.sh does the whole thing in devkitPro's container.
if(NINTENDO_SWITCH AND SWITCH_SOURCES)
    minigolf_add_executable(minigolf-switch "${SWITCH_SOURCES}")
    if(TARGET minigolf-switch)
        target_link_libraries(minigolf-switch PRIVATE nx)
        # The name, author and version the Homebrew Menu shows, and the icon beside them. They
        # are passed to nx_generate_nacp rather than set as variables: it parses its own
        # arguments under the same prefix, which clears any variable of that name first.
        nx_generate_nacp(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/minigolf-switch.nacp
                         NAME "Mini Golf"
                         AUTHOR "iPod recompilation"
                         VERSION ${PROJECT_VERSION})
        nx_create_nro(minigolf-switch
                      NACP ${CMAKE_CURRENT_BINARY_DIR}/minigolf-switch.nacp
                      ICON ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/switch/icon.jpg)
    endif()
endif()

# ---------------------------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------------------------

enable_testing()

# Each tests/unit/*.cpp is a stand-alone program that exits non-zero on failure. They test the
# hand-written code, so a pure-recompilation build — which has no src/game/ — skips them.
if(NOT MINIGOLF_GEN_DIR)
    foreach(TEST_SOURCE ${UNIT_TEST_SOURCES})
        get_filename_component(TEST_NAME ${TEST_SOURCE} NAME_WE)
        add_executable(unit_${TEST_NAME} ${TEST_SOURCE})
        minigolf_link_core(unit_${TEST_NAME})
        add_test(NAME unit_${TEST_NAME} COMMAND unit_${TEST_NAME})
    endforeach()
endif()

# Oracle tests: one per recorded call log. tests/diff.sh compares the headless recomp's
# framework-call log with the emulator's recording of the same input script.
file(GLOB EXPECTED_LOGS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/expected/*.calls)
if(TARGET minigolf-headless AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/diff.sh)
    foreach(LOG ${EXPECTED_LOGS})
        get_filename_component(CASE ${LOG} NAME_WE)
        add_test(NAME oracle_${CASE}
                 COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/diff.sh ${CASE}
                         $<TARGET_FILE:minigolf-headless>
                 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
        # An oracle case needs the player's own copy of the game. Without it diff.sh exits 2,
        # which is a skip rather than a failure: someone who has the source but not the game
        # should still see the unit tests pass.
        set_tests_properties(oracle_${CASE} PROPERTIES SKIP_RETURN_CODE 2)
    endforeach()
endif()

# The pause menu froze the game once, and the oracle could not see it: the recordings were made
# with the emulator, which feeds buttons the same faulty way. This checks the picture is still
# changing after the menu opens.
if(TARGET minigolf-headless AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/pause-menu.sh)
    add_test(NAME pause_menu
             COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/pause-menu.sh
                     $<TARGET_FILE:minigolf-headless>
             WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
    set_tests_properties(pause_menu PROPERTIES SKIP_RETURN_CODE 2)
endif()

# The Cheats screen is this port's own, and the oracle is blind to it by construction (the
# recordings predate it, and --emulator-firmware takes the row away). This drives the screen.
if(TARGET minigolf-headless AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/cheats.sh)
    add_test(NAME cheats_screen
             COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/cheats.sh
                     $<TARGET_FILE:minigolf-headless>
             WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
    set_tests_properties(cheats_screen PROPERTIES SKIP_RETURN_CODE 2)
endif()

# A page could not be left at all, and no recorded case opens one and comes back, so the oracle
# was silent. This presses Menu on the Statistics page and checks the picture changes.
if(TARGET minigolf-headless AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/page-back.sh)
    add_test(NAME page_back
             COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/page-back.sh
                     $<TARGET_FILE:minigolf-headless>
             WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
    set_tests_properties(page_back PROPERTIES SKIP_RETURN_CODE 2)
endif()

# "Return to Menu" opens the save-game card, whose render was a stub that killed the process.
if(TARGET minigolf-headless AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/return-to-menu.sh)
    add_test(NAME return_to_menu
             COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/return-to-menu.sh
                     $<TARGET_FILE:minigolf-headless>
             WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
    set_tests_properties(return_to_menu PROPERTIES SKIP_RETURN_CODE 2)
endif()

# Exit on the main menu asks the firmware to put the game away; here that means ending.
if(TARGET minigolf-headless AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/exit.sh)
    add_test(NAME exit_row
             COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/exit.sh $<TARGET_FILE:minigolf-headless>
             WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
    set_tests_properties(exit_row PROPERTIES SKIP_RETURN_CODE 2)
endif()

# The second oracle: the same script through the hand-decompiled game and the pure recompilation,
# which needs no recording (tests/vs-recomp.sh explains why that matters). One test per script in
# tests/scripts/ that has no recording of its own.
#
# A script written to drive something only this port has is not paired: the pure recompilation is
# the original code and does not have it, and the decompiled side is told to hide it for the
# comparison (--no-port-additions), so the two would be walking a menu neither script step means.
# `cheats` is driven by tests/cheats.sh instead, which is where it belongs.
set(PORT_ONLY_SCRIPTS cheats)
if(TARGET minigolf-headless AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/vs-recomp.sh)
    file(GLOB PAIR_SCRIPTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/scripts/*.script)
    foreach(SCRIPT ${PAIR_SCRIPTS})
        get_filename_component(CASE ${SCRIPT} NAME_WE)
        if(CASE IN_LIST PORT_ONLY_SCRIPTS)
            continue()
        endif()
        if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/expected/${CASE}.calls)
            add_test(NAME recomp_${CASE}
                     COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/vs-recomp.sh ${CASE}
                     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
            set_tests_properties(recomp_${CASE} PROPERTIES SKIP_RETURN_CODE 2)
        endif()
    endforeach()
endif()

# ---------------------------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------------------------

find_program(CLANG_FORMAT NAMES clang-format HINTS /opt/homebrew/opt/llvm/bin)
if(CLANG_FORMAT)
    file(GLOB_RECURSE FORMAT_SOURCES CONFIGURE_DEPENDS
         ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.h
         ${CMAKE_CURRENT_SOURCE_DIR}/src/*.mm ${CMAKE_CURRENT_SOURCE_DIR}/tests/unit/*.cpp)
    add_custom_target(format COMMAND ${CLANG_FORMAT} -i ${FORMAT_SOURCES}
                      COMMENT "clang-format over hand-written sources")
endif()
